Skip to content

dev v0.0.16 - ship stats, bugfixes#25

Open
def9a2a4 wants to merge 62 commits into
mainfrom
dev-stats
Open

dev v0.0.16 - ship stats, bugfixes#25
def9a2a4 wants to merge 62 commits into
mainfrom
dev-stats

Conversation

@def9a2a4

@def9a2a4 def9a2a4 commented May 12, 2026

Copy link
Copy Markdown
Owner

def9a2a4 added 14 commits May 11, 2026 21:23
…d/rotation scaling

Ships now derive their speed, acceleration, and rotation from a power-to-mass
ratio based on sail composition. Wool blocks (3 pts) and banners (7 pts)
provide sail power; every ship gets 2 free base points. The ratio linearly
interpolates between absolute floors (1 block/sec speed, 30s/revolution
rotation) and caps (1.5x default stats), with ratio 0.7 mapping to current
defaults. Sails cap at ratio 0.8 — engines (not yet implemented) will be
needed to push past it.

Airship vertical speed scales with density magnitude rather than the
horizontal ratio.

Also adds ship_engine custom item definition (8 copper + blast furnace recipe)
and updates the ship info display to show wool/banner counts, power ratio, and
effective speed percentage.
Engine system: tagged blast furnace crafted from 8 copper + blast furnace.
BlockPlaceEvent listener transfers PDC tag to TileState, vanilla smelting
suppressed on engine blocks. Scanner and detection count engines via PDC
check. Engine power (30 pts/engine) wired into the ratio calculation for
both horizontal and airship vertical stats. Ship info display and detection
chat messages now show engine count and engine-adjusted power ratio.

Also extracts hardcoded help book content from ShipWheelMenu into
HelpBookContent.java + help_book.yml, loaded once at plugin startup.
Adds the full engine fuel system on top of the engine detection foundation.
Engines now require fuel to contribute power to the ship's ratio. Includes:

- EngineMenuGUI: 3 fuel slots per engine, opened by right-clicking the
  engine block on an assembled ship. Validates fuel-only items, saves on close.
- Fuel consumption: burns 1 tick per game tick while W held. Auto-consumes
  next fuel item when current burns out. Recomputes effective stats on change.
- Smoke particles: CAMPFIRE_SIGNAL_SMOKE at fueled engine positions every
  5 ticks while ship has a driver.
- Fuel state persistence: per-engine fuel slots and burn ticks serialized
  to ship_wheels.yml via Base64-encoded ItemStack bytes.
- Engine block indices and local positions tracked in ShipModel for click
  detection and particle spawning.
- ShipInstance now holds a wheelData reference (set during assembly) for
  fuel state access in the physics loop.

Note: DisplayShip.java and config.yml include unrelated changes from a
concurrent Captain's Manual fix branch (shapeless recipes, help lore).
- Reload help book content on /blockships reload
- Support shapeless recipes in ItemUtil.registerItemRecipe()
- Replace verbose help lore (full book dump) with terse controls summary
- Remove unused getHelpSections() from ShipWheelMenu
- Fix speed percentage display: divide by sail cap (0.8) not default (0.7)
  so 100% means "max sails" rather than "default speed"
- Bump floor-acceleration from 0.005 to 0.015 (less sluggish minimum)
…ne glint

- Simplify ship info book hover to show only speed % (detailed breakdown
  moved to a new Ship Stats banner item at slot 20)
- Speed % now uses sail cap (0.8) as 100% baseline instead of default
  ratio (0.7) — over 100% means engines are contributing
- Stats banner shows wool/banner/engine counts, sail power with "capped
  at 80%" indicator when applicable, mass, power ratio, and speed %
- Add config-driven enchantment glint support to CustomItem (enchant-glint
  field); ship_engine gets glint by default
- Fix floor acceleration default mismatch (config says 0.015, code
  fallback said 0.005)
Previously only ship_wheel and ship kits were giveable. Now supports:
- captains_manual (written help book)
- any custom-items entry (ship_engine, balloon, etc.)

Also adds tab completion for all giveable items and extracts
item listing into shared helpers to avoid duplication.
New config keys added in plugin updates (like ship_engine, stats section)
were invisible to existing servers because Bukkit's saveDefaultConfig()
never overwrites an existing config.yml. Users had to manually delete
their config to get new entries.

migrateConfig() now runs at startup: loads the jar's bundled config,
walks all leaf keys, and adds any that are missing from the user's
config. Never overwrites existing values — customizations are preserved.
Controlled by `auto-migrate-config: true` (opt-out by setting false).
Speed % now color-coded: red (<50%), gold (50-74%), yellow (75-99%),
green (100-124%), aqua (125%+). Ships below 50% speed show a hint to
add sails. Density line now combines the numeric value with a colored
float status label (e.g., "1.33 (Floats well)") instead of separate
lines. Removed surface offset from hover (redundant).

Applied consistently across ship info hover, stats banner, and
detection chat messages.
…er ref

- Engine blocks now drop the custom ship engine item (with PDC + glint)
  instead of a vanilla blast furnace when broken
- Add totalPositiveWeight field to ShipModel so assembled ships report
  correct positive weight instead of passing clamped maxHealth
- Guard computeStat() against divide-by-zero when defaultRatio >= 1.0
- Use local plugin reference for engine PDC check in scanner instead of
  redundant global lookup
Rename totalPositiveWeight → mass (sum of max(0, weight) per block).
This is the correct denominator for the power-to-mass ratio: it
represents how much solid material sails need to push, excluding
negative-weight floatation blocks.

Fixes airships getting zero sail benefit — getSailRatio() previously
returned 0 for negative totalWeight. Now uses mass, so airships with
sails correctly get horizontal speed benefit.

Also fixes:
- engineBlockIndices changed from Set to List so iteration order
  matches engineLocalPositions (fixes smoke at wrong engine / IOOBE)
- Lazy resolveWheelData() for chunk-recovered ships — looks up via
  ShipWheelManager.getWheelByShipUUID() on first access so fuel state
  is correctly loaded instead of assuming all engines fueled
- Shift-click non-fuel into engine GUI now blocked
- Dried kelp burn time 4001 → 4000
- YAML key renamed to "mass" with backwards-compat read of old
  "total_positive_weight" key
…er ref

Engine PDC preserved on disassembly (is_engine flag in rawYaml, restored
in placeBlocks). Ship info uses countFueledEngines() for ratio and shows
fueled/unfueled engines separately. Smoke particles now spawn at engine
shulker position instead of manual coordinate transform. Placed (unassembled)
engine blocks open the custom fuel GUI instead of vanilla furnace UI.
Shift-click non-fuel into engine GUI blocked. Smoke changed to
CAMPFIRE_COSY_SMOKE for shorter duration.

Known issues still unfixed:
- countFueledEngines() doesn't know total engine set (uses computeIfAbsent
  side effect, phantom map entries); needs engine indices parameter
- getShipInfo() uses stale lastDetected* fields for assembled ships instead
  of live ShipInstance data — mass can be 0/stale, effective power = mass
- Engine fuel state and detection data are disconnected systems
- Chat detection messages don't match lore format (missing mass, ratio)
- Unassembled ships show engines at full potential points, not as unfueled
…back

Root cause: assembleShip() never called setLastHealth() or
setLastDetectedStats(), so the ship wheel menu fell into the
"unassembled" display branch (lastMaxHealth=0) even for assembled
ships. This showed all engines at full potential points instead of
the fueled/unfueled breakdown.

Fixes:
- Set lastHealth + detection stats during assembly so menu has
  correct data immediately
- getShipInfo() now reads live ShipInstance data (model + fuel state)
  for assembled ships instead of stale lastDetected* fields
- countFueledEngines() takes engine block indices list so it checks
  ALL engines, not just those with map entries
- getEngineFuelSlots() no longer uses computeIfAbsent (was creating
  phantom entries in the fuel map)
- Physics fallback changed from engineCount (all fueled) to 0
  when wheelData is null
- tickEngineFuel skips engines with no fuel map entry
- Engine display always shows fueled/unfueled breakdown regardless
  of assembled state
- Engine GUI status shows Running/Ready/Idle based on burn ticks
  AND fuel items in slots
@def9a2a4 def9a2a4 added the help wanted Extra attention is needed label May 26, 2026
@def9a2a4 def9a2a4 linked an issue May 26, 2026 that may be closed by this pull request
def9a2a4 added 13 commits June 2, 2026 12:16
…tion deceleration scaling

Previously wool power (3) and banner power (7) were hardcoded across
multiple files. Fuel burn duration had no multiplier. Rotation
deceleration used raw config values with no ratio-based scaling.

Add wool-power, banner-power, fuel-burn-multiplier,
floor-rotation-deceleration, and cap-rotation-deceleration to
config.yml and ShipConfig. Consumers updated in subsequent commits.
…tation decel

- Defer computeEffectiveStats() from constructor to after wheelData is
  linked. Previously the first stat computation always saw 0 fueled
  engines because wheelData wasn't set yet. Now recomputeStats() is
  called explicitly after assembly and lazily on recovery.
- Guard minMovementThreshold so it only zeros speed when W/S aren't
  held — prevents low-ratio ships from being unable to move.
- Return empty bucket when lava bucket fuel is consumed (vanilla parity).
- Burn fuel on any movement input (A/D/Space/Sprint), not just W.
- Apply fuel-burn-multiplier from config when consuming fuel items.
- Scale rotation deceleration by power-to-mass ratio so heavy ships
  retain rotation momentum longer.
Engine fuel:
- Remap GUI fuel slots {1,2,3} → {0,1,2} for direct 1:1 mapping with
  blast furnace container indices. Status slot moves from 5 → 4.
- Transfer pre-assembly fuel from blast furnace containers into
  wheelData on assembly (was silently lost).
- Write wheelData fuel back to containers on disassembly (reverse gap).
- Clear stale fuel/burn entries on disassembly.
- Stop clearing entire blast furnace on save — targeted slot writes only.
- Crash-safe fuel deserialization with per-item try-catch.
- Add click-to-refresh on engine status item.

Stats display:
- Use weighted block count for density (matches physics, was using total
  block count which included weightless blocks like trapdoors).
- Use config values instead of hardcoded 0.8 sail cap, 2 base power,
  3 wool power, 7 banner power across all display paths.
- Standardize chat terminology: "power" → "pts".

Detection chat:
- Add chat output for assembled ship detection (was completely silent).
- Show live fuel state (fueled/unfueled engine breakdown) for assembled.
… dead data

Engine GUI validation:
- Block double-click collect and number-key hotbar swaps with non-fuel
  items. Add InventoryDragEvent handler to prevent drag-placing non-fuel.
- Recompute ship stats on engine GUI close so fuel changes take effect
  immediately without requiring movement.

Engine explosions:
- Handle EntityExplodeEvent and BlockExplodeEvent for engine blocks —
  drop custom ship engine item instead of vanilla blast furnace.

Null safety:
- Guard null/invalid shulkers in camera distance update loop.

ShipModel cleanup:
- Remove dead engineLocalPositions field (populated but never read;
  smoke particles use collision shulker positions instead).
- Accept woolPower/bannerPower as constructor params so sail power
  calculation uses config values. Old YAML with engine_local_positions
  key is silently ignored on load.
computeEffectiveStats() previously called ship.resolveWheelData(),
which could trigger recomputeStats() → computeEffectiveStats() again
when wheelData was first lazily resolved. The null guard prevented
infinite recursion but the double computation was wasteful and fragile.

Read ship.wheelData directly instead — all callers of recomputeStats()
(assembly, recovery, GUI close) guarantee wheelData is set beforehand.
…orms (1/3)

Root cause: the entity tracker sends vehicle rotation at byte precision
(~1.4° quantization) every 3 ticks, conflicting with float-precision
position sync packets sent every tick — creating periodic jitter.

Fix: freeze the vehicle's yaw at spawnYaw and track rotation internally
via physics.currentYaw. All visual rotation is applied through display
entity transformation matrices, bypassing the entity tracker entirely.

Changes:
- ShipPhysics: add currentYaw field, rotation updates it instead of
  teleporting the vehicle, forward direction and snap methods use it
- ShipInstance: all vehicle.getYaw() → physics.currentYaw, removed
  version-specific display rotation branch (always apply delta rotation
  via transformation), added idle yaw sync and setInterpolationDelay(0)
- ShipCollision: collision forward direction uses physics.currentYaw

Known issues to fix in follow-up commits:
- idle yaw sync causes double rotation when ship stops (display
  transformation not updated on idle tick but vehicle yaw jumps)
- setInterpolationDelay(0) causes item display Y jitter (interpolation
  duration > update interval creates cascading positional lag)
- DisplayShip dismount velocity uses frozen vehicle yaw (wrong direction)
- ShipPersistence saves frozen vehicle yaw (loses rotation on chunk save)
- currentYaw can grow unbounded without normalization
Issues observed during in-game testing of the internal yaw tracking:

- idle yaw sync caused double rotation on stop: when the ship went idle,
  the vehicle yaw was teleported from spawnYaw to currentYaw, but the
  display transformation (still carrying deltaYaw) was not updated on
  idle ticks — resulting in visual = inheritedYaw + deltaYaw = 2x rotation.
  Fix: remove idle yaw sync entirely, vehicle yaw stays frozen forever.

- setInterpolationDelay(0) caused item display Y jitter: with
  interpolation_duration=2 but updates every 1 tick, each interpolation
  was interrupted before completion, creating cascading positional lag
  visible as massive downward jitter on item displays.
  Fix: remove setInterpolationDelay(0), let transforms snap at 20 Hz
  (still 3x smoother than the original 6.67 Hz byte-precision tracker).

- dismount velocity used frozen vehicle yaw: players ejected from a
  rotated ship would fly in the original spawn direction.
  Fix: use physics.currentYaw in DisplayShip dismount calculation.

- persistence saved frozen vehicle yaw: chunk unload would lose the
  ship's actual rotation.
  Fix: save physics.currentYaw in ShipPersistence.fromInstance().

- snapToFineGrid changed vehicle yaw: driver exit would unfreeze the
  vehicle yaw, re-introducing entity tracker byte-precision packets.
  Fix: only snap position, keep vehicle yaw frozen.

Remaining: currentYaw can grow unbounded without normalization.
Custom ships lost their rotation on chunk reload because recoverEntities()
set spawnYaw to model.initialRotation.x (assembly yaw), but the idle sync
had already reset spawnYaw to currentYaw. This mismatch created a non-zero
deltaYaw on recovery, causing double rotation (inherited vehicle yaw +
transformation delta both contributing rotation).

Fix: always set spawnYaw = vehicle.getYaw() on recovery (for all ship
types), matching the idle sync's invariant that spawnYaw == currentYaw.
deltaYaw is 0 on recovery, display shows R(initial), and the vehicle's
inherited yaw provides the actual rotation.

Also:
- Extract updateDisplayTransforms() method from tick() so the idle yaw
  sync can call it after resetting spawnYaw (ensures display matches
  the synced state immediately, preventing one-tick visual glitch)
- Add idle yaw sync: when ship stops, sync vehicle yaw to currentYaw
  for NBT persistence, reset spawnYaw to currentYaw, update displays
- Normalize currentYaw to [0,360) after each rotation update
- Use normalizeAngle() for position sync yaw delta to handle wrapping
- Normalize spawnYaw/currentYaw at all init sites
…oGrid (4/3)

The idle yaw sync teleported the vehicle on the first idle tick to update
its NBT yaw for chunk persistence. This caused a 1-frame visual pop:
the ENTITY_TELEPORT packet (changing vehicle yaw) and the display entity
metadata update (resetting transform delta to 0) arrive at the client as
separate packets, momentarily showing double-rotation.

Root fix: save physics.currentYaw in per-world YAML metadata
(ShipWorldData) so chunk recovery no longer depends on vehicle entity
NBT yaw. The idle sync teleport is then unnecessary and removed entirely.
Vehicle yaw now stays frozen at spawnYaw for the entity's entire lifetime.

Metadata uses config.contains() to distinguish legacy files (no
current_yaw key → Float.NaN sentinel → fall back to vehicle NBT) from
files where the ship genuinely faces yaw=0 (north).

Also fixes alignToGrid(): the vehicle teleport was passing snappedYaw,
unfreezing the vehicle yaw. Now preserves loc.getYaw() (frozen) and
resets spawnYaw + refreshes display transforms in the ShipInstance
wrapper. ShipWheelManager.disassembleShip() reads physics.currentYaw
instead of the frozen vehicle yaw for block placement rotation.
BACKGROUND

The rotation system (82bda96) freezes the vehicle entity's yaw at
spawnYaw for its entire lifetime. All visual rotation is applied through
display entity transformation matrices using:

  deltaYaw = physics.currentYaw - spawnYaw

On 1.21.9+, Minecraft clients inherit the parent vehicle's yaw when
rendering display entity passengers. This means the client renders:

  visual rotation = vehicle.getYaw() + deltaYaw + initialModelRotation
                  = spawnYaw + (currentYaw - spawnYaw) + initial
                  = currentYaw + initial  ✓

This is only correct when the invariant holds:

  spawnYaw == vehicle.getYaw()

If spawnYaw is reset to a value that differs from vehicle.getYaw(),
the client's inherited vehicle rotation no longer cancels out and the
ship appears at the wrong angle.

WHAT 1648bb5 BROKE

1648bb5 replaced the idle yaw sync with per-world metadata persistence
(saving physics.currentYaw to ShipWorldData YAML). The goal was correct —
the idle sync caused a 1-frame double-rotation pop because the
ENTITY_TELEPORT packet (updating vehicle yaw) and the display metadata
packet (resetting deltaYaw to 0) arrived at the client in separate frames.
Metadata persistence avoids this by never changing vehicle yaw at all.

However, 1648bb5 introduced two bugs that break the invariant:

BUG 1 — alignToGrid() froze vehicle yaw while resetting spawnYaw

  alignToGrid() in ShipPhysics was changed to pass loc.getYaw()
  (the frozen vehicle yaw) in the teleport Location instead of
  snappedYaw. The ShipInstance wrapper then does:

    spawnYaw = physics.currentYaw;  // = snappedYaw

  This left vehicle.getYaw() at the old frozen value while spawnYaw
  was updated to snappedYaw — breaking the invariant.

  After align (before fix):
    vehicle.getYaw() = oldFrozenYaw  (e.g. 47°)
    spawnYaw         = 90°  (reset in wrapper)
    deltaYaw         = 0°   (currentYaw - spawnYaw)
    client renders   = 47° + 0° = 47°  ← wrong

  In-game effect: after using the align command, the ship's display
  blocks would visually snap to their spawn orientation (the old frozen
  yaw), while the collision boxes remained at the correct snapped angle.
  Invisible walls next to visible blocks.

BUG 2 — chunk recovery set spawnYaw from metadata instead of vehicle NBT

  recoverEntities() was changed to set BOTH spawnYaw and currentYaw
  from the metadata yaw:

    spawnYaw         = metadataYaw  (e.g. 90°)
    currentYaw       = metadataYaw  (e.g. 90°)
    deltaYaw         = 0°

  But the vehicle entity loads from chunk NBT with its original frozen
  yaw (e.g. 0°), which the client sees. So:

    vehicle.getYaw() = 0°   (from NBT — was never updated)
    spawnYaw         = 90°  (from metadata)
    deltaYaw         = 0°
    client renders   = 0° + 0° = 0°  ← should be 90°

  In-game effect: every ship that had been rotated from its spawn angle
  would appear at the wrong rotation after chunk unload/reload. Walking
  away from a ship and returning would show it "unrotated" back to its
  spawn angle, while collisions remained at the correct rotated position.

FIXES

Fix 1 — revert alignToGrid() to update vehicle yaw (ShipPhysics.java)

  Restore snappedYaw in the teleport Location for alignToGrid(). The
  ship is stationary after align, so there is no active position-sync
  packet to conflict with the rotation packet — no byte-precision jitter.
  Cardinal angles (90° multiples) also map exactly to byte encoding
  (90/360 x 256 = 64), so there is zero quantization error.

  After align with fix:
    vehicle.getYaw() = 90°  (updated by teleport)
    spawnYaw         = 90°  (reset in wrapper)
    currentYaw       = 90°
    deltaYaw         = 0°
    client renders   = 90° + 0° = 90°  ✓

  Invariant maintained. The ShipWheelManager change reading
  ship.physics.currentYaw for block placement remains correct (same
  value as vehicle.getYaw() post-align, but more explicit).

Fix 2 — recovery sets spawnYaw from vehicle NBT, currentYaw from metadata
         (ShipInstance.java recoverEntities)

  spawnYaw must equal vehicle.getYaw() (what the client inherits from
  NBT). The metadata yaw goes into currentYaw, making deltaYaw provide
  the compensation:

    spawnYaw   = normalizeYaw(vehicle.getYaw())   // e.g. 0° (NBT)
    currentYaw = normalizeYaw(metadataYaw)         // e.g. 90°
    deltaYaw   = 90° - 0° = 90°
    client     = 0° + 90° = 90°  ✓

  Legacy metadata without current_yaw falls back to spawnYaw, giving
  deltaYaw = 0 — identical to pre-metadata behaviour.

Fix 3 — replace R_initial-only init display loop with updateDisplayTransforms()
         (ShipInstance.java recoverEntities)

  The previous init loop applied only R_initial (no delta). With fix 2
  making spawnYaw != currentYaw, this would cause a 1-tick flash at the
  wrong angle before the first tick applied the correct transform.
  Replacing the 18-line manual loop with updateDisplayTransforms() applies
  the full deltaYaw immediately on recovery, eliminating the flash. The
  call is safe at this point: vehicle, displays, spawnYaw, currentYaw,
  cachedR_initial, and all work matrices are fully initialized.

WHAT REMAINS CORRECT FROM 1648bb5

  - ShipWorldData saving physics.currentYaw and loading via
    config.contains() (correct sentinel, handles yaw=0 case)
  - Idle sync removal (safe — spawnYaw is never reset during normal
    ticking, so the invariant holds without it)
  - alignToGrid() wrapper (spawnYaw reset + updateDisplayTransforms)
  - ShipWheelManager reading physics.currentYaw for block placement
Adds `custom-ships.destruction-mode` to config.yml with two values:

- `disassemble` (default): current behavior — blocks placed back into the
  world, wheel block broken and dropped, explosions spawned.
- `destroy`: ship is permanently lost. Stored inventory contents and engine
  fuel drop as loose items at the ship's location; blocks and the wheel item
  are not recovered. Explosions still spawn.

Implementation:
- ShipConfig: adds `destroyOnDeath` boolean field loaded from
  `custom-ships.destruction-mode` (true iff value == "destroy").
- ShipInstance.destroyAndDropItem(): when destroyOnDeath is set, drops
  storages and fuel then calls destroyWithCleanup() instead of routing
  through disassembleShip(). Prefab ships are unaffected (branch is inside
  the "custom".equals(shipType) guard).
- ConfigValidator.migrateConfig() picks up the new key automatically via
  its full-key scan of the bundled config.

Three bugs are fixed in the following commit (found during review):
- destroyWithCleanup() called without null-guarding getDisplayShip()
- wheel block left in-world and placedWheels map not cleaned up on destroy,
  causing the wheel to reappear after server restart
- lead items silently lost when ship shulkers are removed; players lose
  leads with no feedback or item drop
def9a2a4 added 30 commits June 7, 2026 13:15
The ship stats system (5da91fd) introduced power-to-mass ratio scaling.
Custom test ships had no wool or banners beyond one existing banner,
giving them a ratio of ~0.19 (power 9 / mass 47). At this ratio ships
crawl at floor speed, and the test's forward+backward control sequence
results in near-zero net displacement (total=1.37, need >=2).

Add 3 banners and 1 wool block to the surface layer of both custom_ship
and custom_airship test builds. This raises custom_ship's ratio to ~0.53
(power 26 / mass 49), well above the floor and close enough to the
default 0.7 to move at reasonable speed through the test sequence.
…tity scope

PlayerInput: handle dismount explicitly via dismountPlayer() instead of
relying on PlayerToggleSneakEvent (not guaranteed in vehicles). Expand
registration details and threading analysis.

Tile entities: scope down to chiseled bookshelves + signs only. Correct
API usage from code review (getSnapshotInventory, serializeInventory
signature). Note existing container item duplication bug to fix.
On Paper 1.21.2+, use the native PlayerInputEvent API for ship
steering instead of ProtocolLib packet interception. Falls back to
ProtocolLib on older servers.

This also fixes the 1.21.3-1.21.8 gap where ProtocolLib reflection
on the Input record failed, and eliminates a latent JMM race condition
(ProtocolLib writes input booleans from the netty thread while physics
reads them on the main thread — both are plain non-volatile fields).
Shelves (1.21.9+, 12 variants) and chiseled bookshelves now allowed in
custom ships. Both use TileStateInventoryHolder (not Container), so a
single instanceof check handles serialization and restoration. Inventory
cleared before block removal to prevent item duplication. Shelf items
show as empty BlockDisplay during flight; items restored on disassembly.

Sign text (front/back lines, dye color, glow, waxed state) now
serialized on assembly and restored on disassembly. Text still not
visible on BlockDisplay during flight (Minecraft limitation).
…elf rotation

Destroy-on-death path now drops items from shelves and chiseled bookshelves.
These blocks use TileStateInventoryHolder (not Container) so their items
live in rawYaml["container_items"] rather than the storages map. The new
loop iterates sourceModel.parts, guarded by part.storage == null to avoid
double-dropping items already handled by the Container/storages path.

Also adds display_rotation: true to chiseled_bookshelf and *_shelf entries
in blocks.yml — these are directional blocks whose facing is ignored by
BlockDisplay entities, so the ship system needs to apply manual Y-axis
rotation transforms (same mechanism used for chests and barrels).
Container inventories were serialized into rawYaml but never cleared from
the world block. When removeBlocks() later calls setType(AIR), the block
drops its items — duplicating items already captured in the serialized data.

Fix: clear the snapshot inventory and call update() after serialization to
push the empty state to the world block before removal. Applied to both
the Container path (chests, furnaces, hoppers) and TileStateInventoryHolder
path (shelves, chiseled bookshelves).

Also corrects the TileStateInventoryHolder clearing to use
getSnapshotInventory().clear() + update() instead of getInventory().clear()
— the snapshot-then-update pattern is the correct Bukkit API usage and
matches the Container path.
…eview

Mark chiseled bookshelves, shelves, and signs as done in the tile entity
feature item. Add three code quality items identified during review:
Container/TileStateInventoryHolder double-match in BlockStructureScanner
(with brewing stand edge case note), uncached block.getState() calls in
serialization loop, and unused plugin param in PaperInputListener.
Airships need more time to decelerate vertically after chunk recovery,
especially with PaperInputListener (main-thread input vs ProtocolLib's
netty thread). Settle wait 3s→5s, post-dismount wait 1s→2s.
… settle

setControlState only sends a packet when state changes — if already
false, no packet fires and the plugin retains stale input from the
last steerShip tick. Send explicit all-false player_input packet to
guarantee the plugin clears input state. Also increase settle wait
3s→10s and post-dismount wait 1s→5s for slow CI hardware.
Bot was getting kicked with "Failed to decode packet
'serverbound/minecraft:accept_teleportation'" on 1.21.11 servers.
The installed minecraft-data (3.102.3) only had protocol data up to
1.21.8 — the teleportation confirmation packet format changed in
newer versions and the bot was sending a malformed packet.

mineflayer 4.37.1 pulls minecraft-protocol 1.66.2 and minecraft-data
3.110.2, which includes 1.21.9 and 1.21.11 protocol definitions.
Root cause: mineflayer's bot.moveVehicle() sends player_input with
forward/backward/left/right but omits jump. bot.setControlState('jump')
only sets a local physics flag (jumpQueued) without sending any packet.
On 1.21.2+ where PaperInputListener processes PlayerInputEvent, the
server never receives jump input — airships can't ascend.

The steerShip helper had a workaround for 1.21.8 specifically
(useRawPlayerInput = bot.version === '1.21.8') that sends raw
player_input packets with all fields. Extend this to all 1.21.2+
versions where the new player_input packet format is used. Also add
sprint field for completeness (used for airship descent).

This fixes smallairship, custom_airship, and chunk_persistence_airship
CI failures on 1.21.2+ servers.
With the jump input fix (13e84cb), airships now properly receive jump
input on 1.21.2+ and accelerate much more than before. The 2s climb
duration built up too much vertical velocity for the 10s settle period
on slow CI hardware. Reduce to 500ms — the test only needs to verify
the ship moved, not that it moved far.
mineflayer's bot.entity.position doesn't update while riding a vehicle.
After dismount, the plugin teleports the player to a safe position near
the ship, but mineflayer may not process the teleport packet in time.
For airships that climbed vertically, movedPos was stuck at the pre-mount
Y=105 while the ship was at Y≈170 — the test then teleported back to
ground level after the chunk cycle and measured 66 blocks to the ship.

Send /tp @s ~ ~ ~ after dismount to force a server→client position
packet so mineflayer syncs before recording movedPos.
Root cause: for airships, the plugin teleports the dismounting player
to ground level (Y≈105) while the ship stays at altitude (Y≈160+).
The test used bot.entity.position as the reference point, then compared
shulker positions against it after a chunk cycle — measuring 57-66
blocks of "drift" that was actually just the vertical gap between
ground level and the airship.

Fix: compute average shulker position before the chunk cycle and use
that as the reference point. Teleport back to the ship's position
(not the bot's ground-level dismount position) for the chunk cycle.

Also add diagnostic logging: shulker positions with full XYZ at three
points (right after dismount, 1s later, after teleport back) to detect
any actual drift during chunk recovery.
…ulker positions

Shulkers are passengers of carrier ArmorStands. The MC server does not
send position update packets for passenger entities — the vanilla client
calculates their render position from the vehicle. Mineflayer does not
implement this client-side positioning, so bot.entities[id].position for
shulkers stays frozen at assembly-time coordinates after ship movement.

The chunk_persistence test steers the ship ~36 blocks forward, dismounts,
reads shulker positions (stale, still at assembly coords Z≈-2), cycles
chunks, then reads shulker positions again (fresh from spawn packets,
true position Z≈-35). The 36-block "drift" was never real — it was the
distance traveled during steering, measured correctly post-cycle but
compared against stale pre-cycle data.

Confirmed from CI logs (run 27178642083, paper 1.21.4):
  Pre-cycle shulker avg (stale):  Z = -1.7  (assembly position)
  Bot dismount pos (server truth): Z = -35.6 (36.2 blocks from start)
  Post-cycle shulkers (fresh):     Z = -34.5 to -36.5
  Bot position ≈ post-cycle shulkers. Pre-cycle was stale.

The root cause was commit d3c190b which switched the pre-cycle reference
from bot.entity.position (server truth via plugin safe-teleport + /tp
sync) to avgShulkerPos (stale passenger positions), to work around the
airship Y-offset on dismount (bot teleported to ground, ship at altitude).

Fix: add getShipEntityPos() which reads shulker.vehicle?.position (the
carrier ArmorStand, a standalone entity with normal position tracking)
with fallback to shulker.position (correct post-reload when spawn packets
provide fresh positions). This works for both water ships and airships
since carrier positions include vertical movement — no Y-offset issue.
On 1.21.3+, bot.dismount() sends player_input { jump: true } which
PaperInputListener interprets as climb input, not dismount. This leaves
isSpacePressed=true on the driverless ship, causing indefinite vertical
acceleration (~39 blocks of drift in CI).

Test fix (helpers.js): on 1.21.3+, try raw player_input { shift: true }
first (correct PaperInputListener dismount), demote bot.dismount() to
fallback, remove the { jump: true } fallback entirely. Send all-false
player_input after every successful dismount to clear stale input state.

Plugin fix (ShipPhysics.java): gate isSpacePressed/isSprintPressed on
hasDriver in applyAirshipVerticalPhysics() so a driverless airship never
responds to stale input flags regardless of how they were set.

Also: verify entity counts by type (armor_stand, shulker, block_display,
item_display) across chunk cycles, remove 1.21.4 from VERSION_SKIPS.
…n check

On 1.21.1, the player_input packet doesn't exist — protodef silently
maps the unknown name to varint 0x00 (teleport_confirm), so the server
receives a zero-payload teleport confirm and kicks the bot. Guard the
write with supportFeature('newPlayerInputPacket') to match the existing
clearShipInput pattern in helpers.js.
26.2 ("Chaos Cubed") released 2026-06-16. Paper (26.2-24) and Purpur
(build 2595) both have builds available. No conditional changes needed:
the workflow already keys Java 25 selection and bot-test skipping off
startsWith(matrix.minecraft, '26.').
In biomes where drowned spawn heavily, the special ship-wheel-dropping drowned appeared in overwhelming numbers. Lower the default spawn-chance from 0.05 to 0.02, keeping the config default and the code fallback in sync. The feature can still be disabled or tuned via config.
…ization

- TODO-land-vehicles.md: design for Land Vehicle Wheel and horse-style step-up movement, reusing the existing custom-ship type plumbing (#10)
- TODO-profiling.md: nanoTime instrumentation + /blockships perf command to measure tick budget, split by terrain vs ship-to-ship collision
- TODO-skip-interior.md: skip spawning collider pairs for fully-enclosed interior blocks to cut per-tick entity overhead
- TODO-ci.md: root-cause writeup for the chunk_persistence test failures (stale mineflayer positions, a test bug — not ship drift)
- TODO-playerinput.md: add 26.2 to the documented CI version matrix
- cross-reference issues v0.0.16 resolves or partially addresses: #28 (optional ProtocolLib), #23 (shelves/bookshelves/signs, partial), #7 (chunk-reload rotation snap, partial), #29 (rarer drowned captains)
- note the Captain's Manual is now a craftable item
- add a "Known Issues / Not Yet Addressed" section covering #20 (wall heads), #23 (pots/other tile entities), #24 (partial destruction), #7 (legacy collider desync), and #28 (pending reporter confirmation)
- TODO.md: add deck-physics note (carry standing players with moving ships)
Bukkit.getBukkitVersion() on Paper 26.2 returns "26.2.build.24-alpha", a
new API-version format. The old parser split on "-" then "." and called
Integer.parseInt() on every resulting segment, so parts[2] = "build" threw
NumberFormatException and the parser fell back to the hardcoded 1.21.11.

This surfaced in the 26.2 CI startup test as:
  [ServerVersion] Could not parse version '26.2.build.24-alpha':
  For input string: "build" - defaulting to 1.21.11
The fallback is logged at SEVERE (-> [ERROR]: [BlockShips] ...), which the
CI error grep treats as a failure, so the 26.2 job went red. (26.1.2 used
the older "26.1.2-R0.1-SNAPSHOT" format and parsed fine.)

Parse only the leading numeric dot-separated segments and stop at the first
non-numeric one, treating any remaining components as 0. "26.2.build.24-alpha"
now yields 26.2.0; existing formats are unchanged (1.21.11-R0.1-SNAPSHOT ->
1.21.11, 1.21 -> 1.21.0). A version with no leading numeric segment still
throws into the unchanged 1.21.11 fallback.

The 1.21.11 fallback was functionally harmless today (all isAtLeast() gates
check thresholds <= 1.21.9, true for both 26.2 and the fallback), but the
parser now reports the real version and no longer logs a spurious error.

Verified the parse logic against 26.2.build.24-alpha, 26.2.0,
26.1.2-R0.1-SNAPSHOT, 1.21.11/1.21.1/1.21, and an unparseable string.
PaperInputListener (added in e15f794) makes ProtocolLib unnecessary on
1.21.2+. Replace the glob copy of all cached plugin jars with explicit
per-plugin copies, guarded by ifeq on MINECRAFT_VERSION for ProtocolLib.

Also pass MINECRAFT_VERSION to the test-server-setup CI step -- without
this, Make's ifeq always sees the default (1.21.11) and ProtocolLib is
never copied, breaking the 1.21.1 matrix entry where it's still required.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

help wanted Extra attention is needed

Projects

None yet

Development

Successfully merging this pull request may close these issues.

destruction - complete destruction, no block placement custom ship speed/stats based on sails, engines Compatibility with Towny / Claiming

1 participant